﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;

namespace WMemoryProfiler.Memory
{
        /// <summary>
        /// Dump one or several processes and places the resulting data into a temp folder.
        /// </summary>
        /// <example>
        /// This class is usually used to dump in failing test cases the current or other processes for deeper investigation.
        /// <code>
        /// using WMemoryProfiler.Memory;
        /// 
        /// [Test]
        /// void FalingTestCase()
        /// {
        ///     try
        ///     {
        ///         // actual test code
        ///     }
        ///     catch(Exception ex)
        ///     {
        ///         ProcessDumper dumper = new ProcessDumper("FalingTestCase",false);  // The string is added to DumpIndex.txt to describe the dumps, do not copy dumps to test output folder
        ///         dumper.Dump(ProcessDumper.FilterSelf);
        ///         throw;
        ///     }
        /// }
        /// </code>
        /// Besides the dump files a DumpIndex.txt file is also created which contains the process name, pid and most importantly the process command line.
        /// This helps to quickly identify which processes are worth investigating first. Below is some sample output of the Dump
        /// Failed Testcase Name: FalingTestCase
        /// 15.09.2014 14:06:48 Dumping process QTAgent32_40 Pid: 116700: Command Line: "C:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\IDE\QTAgent32_40.exe" /agentKey c4421c2a-872c-4d9b-a183-53af9d8769da /hostProcessId 108068 /hostIpcPortName eqt-6622d6bd-cda8-3204-74c6-4d00bd5d06d0
        /// </example>
        /// <remarks>When on the test agent less than 5 GB of free disc space is available no dumps are generated to prevent to make the test agent unusable.</remarks>
        public class ProcessDumpCreator
        {

            /// <summary>
            /// Matches the currently executing process (usually the test agent process)
            /// </summary>
            public static ProcessFilter FilterSelf = new ProcessFilter(Process.GetCurrentProcess().Id);

            // These variables are instance based to allow unit testing
            internal string Temp;
            internal string PathToProcDump;
            internal string TempDumpDir;
            internal string TempDumpIndexFile;


            internal const string DumpIndexFileName = "DumpIndex.txt";
            internal const string MemoryDumpFolderName = "MemoryDumps";

            /// <summary>
            /// Do not create a dump if less than this amount of memory is available on the drive where the TEMP folder is located.
            /// </summary>
            const long MinFreeGB = 5;

            /// <summary>
            /// Failing test name if present. It is added to the DumpIndex.txt file which describes the dump collection.
            /// </summary>
            public string TestName
            {
                get;
                private set;
            }

            /// <summary>
            /// Location in test data output folder where the meory dumps are written to.
            /// </summary>
            public string TestDirDumpFolder
            {
                get;
                internal set;
            }

            /// <summary>
            /// For unit testing the ctor logic
            /// </summary>
            internal static Action<ProcessDumpCreator> ModifyMembersAfterSettingDefaults = null;

            /// <summary>
            /// Enable unit testing of out of disc space condition
            /// </summary>
            internal static Func<long> GetDiscFreeSpaceFromTempTest = null;


            private ProcessDumpCreator()
            {
                Temp = Environment.GetEnvironmentVariable("TEMP");
                PathToProcDump = Path.Combine(Temp, "procdump.exe");
                TempDumpDir = Path.Combine(Temp, MemoryDumpFolderName);
                TempDumpIndexFile = Path.Combine(TempDumpDir, DumpIndexFileName);

                TestDirDumpFolder = TempDumpDir;

                if (ModifyMembersAfterSettingDefaults != null)
                {
                    ModifyMembersAfterSettingDefaults(this);
                }

                WriteProcDumpToTemp();
                Directory.CreateDirectory(TempDumpDir);
                DeleteOldDumps(TempDumpDir);
                Directory.CreateDirectory(TestDirDumpFolder);
                Console.WriteLine("Test Directory Dump folder: {0}", TestDirDumpFolder);
            }

            /// <summary>
            /// Create a new dumper instance.
            /// </summary>
            /// <param name="testName">The failed test name is added in the header of DumpIndex.txt to help you to identify if you look at the dumps of the right test.</param>
            public ProcessDumpCreator(string testName)
                : this()
            {
                TestName = testName;
                if (TestName == null)
                {
                    TestName = "Not Set";
                }
            }

            /// <summary>
            /// Dump all processes which match process name and command line.
            /// </summary>
            /// <param name="processName">process name e.g. Container.exe</param>
            /// <param name="cmdLineSubstring">Substring of command line that must match</param>
            public void Dump(string processName, string cmdLineSubstring)
            {
                if (processName == null && cmdLineSubstring == null)
                {
                    throw new ArgumentException("processName and cmdLineSubstring filter was null.");
                }

                ProcessFilter filter = new ProcessFilter(processName, cmdLineSubstring);
                Dump(filter);
            }

            /// <summary>
            /// Dump all processes with given executable name. 
            /// </summary>
            /// <param name="processName">Process name</param>
            public void Dump(string processName)
            {
                if (String.IsNullOrEmpty(processName))
                {
                    throw new ArgumentException("processName was null or empty");
                }

                ProcessFilter filter = new ProcessFilter(processName);
                Dump(filter);
            }


            /// <summary>
            /// Dump all proceses which match the ProcessFilter query.
            /// </summary>
            /// <param name="filter">ProcessFilter to use</param>
            public void Dump(ProcessFilter filter)
            {
                if (filter == null)
                {
                    throw new ArgumentNullException("filter");
                }

                int count = 0;

                foreach (int pid in filter.GetMatchingPids())
                {
                    Dump(pid);
                    count++;
                }

                if (count == 0)
                {
                    Console.WriteLine("Error: {0}did not match any processes", filter);
                }
            }


            /// <summary>
            /// Dump yourself.
            /// </summary>
            public void DumpOwnProcess()
            {
                Dump(Process.GetCurrentProcess().Id);
            }

            /// <summary>
            /// Dump process with given process id.
            /// </summary>
            /// <param name="pid">pid of process to dump.</param>
            public string Dump(int pid)
            {
                long freeSpace = GetDiscFreeSpaceFromTemp();
                if (freeSpace < MinFreeGB * 1024 * 1024 * 1024)
                {
                    Console.WriteLine("Error: Could not dump process because drive {0} has less than {1} GB free disc space", Temp, MinFreeGB);
                    return null;
                }

                var processToDump = Process.GetProcessById(pid);

                AppendIndexFile(TempDumpIndexFile, processToDump.ProcessName, processToDump.Id, ProcessFilter.GetProcessCommandLine(pid));

                string fullDumpFileName = String.Format("{0}\\{1}_{2}_{3}.dmp", TempDumpDir, processToDump.ProcessName, pid, DateTime.Now.ToString("HH_MM_ss"));


                ProcessStartInfo proc = new ProcessStartInfo(PathToProcDump,
                    String.Format("-accepteula -ma {0} {1}", pid, fullDumpFileName))
                {
                    UseShellExecute = false,
                    RedirectStandardOutput = true
                };

                var process = Process.Start(proc);
                Console.WriteLine(process.StandardOutput.ReadToEnd());
                process.WaitForExit();

                if (!File.Exists(fullDumpFileName))
                {
                    Console.WriteLine("Error: Could not create dump file of process!");
                    return null;
                }
                else
                {
                    return fullDumpFileName;
                }
            }

            private void AppendIndexFile(string tempDumpIndexFile, string processName, int pid, string cmdLine)
            {
                bool addHeader = true;
                if (File.Exists(tempDumpIndexFile))
                {
                    addHeader = false;
                }

                FileStream fileStream = null;

                try
                {
                    fileStream = File.Open(tempDumpIndexFile, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read);
                }
                catch (IOException ex)
                {
                    Trace.TraceInformation("Could not open file {0} due to {1}", tempDumpIndexFile, ex);
                    // if file is locked append pid to index file name
                    fileStream = File.Open(tempDumpIndexFile + "_" + Process.GetCurrentProcess().Id, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read);
                }

                using (fileStream)
                {
                    fileStream.Seek(0, SeekOrigin.End);
                    using (var writer = new StreamWriter(fileStream))
                    {
                        if (addHeader)
                        {
                            WriteIndexFileHeader(writer);
                        }

                        writer.WriteLine("{0} {1} {2} Dumping process {3} Pid: {4}: Command Line: {5}", DateTime.Now.ToShortDateString(),
                            DateTime.Now.ToLongTimeString(),
                            Environment.MachineName,
                            processName, pid, cmdLine);
                    }
                }
            }

            private void WriteIndexFileHeader(StreamWriter writer)
            {
                writer.WriteLine("Failed Testcase Name: {0}", this.TestName);
            }

            internal void WriteProcDumpToTemp()
            {
                File.WriteAllBytes(PathToProcDump, Resource1.procdump);
            }

            long GetDiscFreeSpaceFromTemp()
            {
                if (GetDiscFreeSpaceFromTempTest != null)
                {
                    return GetDiscFreeSpaceFromTempTest();
                }

                string root = Path.GetPathRoot(Temp);
                var driveInfo = DriveInfo.GetDrives().Single(drive => drive.RootDirectory.Name.ToUpper() == root.ToUpper());
                return driveInfo.AvailableFreeSpace;
            }

            private void DeleteOldDumps(string tempDumpDir)
            {
                foreach (var file in Directory.GetFiles(tempDumpDir, "*.dmp"))
                {
                    try
                    {
                        File.Delete(file);
                    }
                    catch (Exception ex)
                    {
                        Trace.TraceInformation("Could not delete old dump file {0}: {1}", file, ex);
                    }
                }

                if (File.Exists(TempDumpIndexFile))
                {
                    try
                    {
                        File.Delete(TempDumpIndexFile);
                    }
                    catch (Exception ex)
                    {
                        Trace.TraceInformation("Could not delete old dump index file {0}: {1}", TempDumpIndexFile, ex);
                    }
                }
            }
    }
}
